﻿#region << 版 本 注 释 >>
/*----------------------------------------------------------------
 * 版权所有 (c) 2022 北京超维景生物科技有限公司 保留所有权利。
 * CLR版本：4.0.30319.42000
 * 文件名：Kernel
 * 
 * 创建者：huangyang
 * 电子邮箱：huangyang@tvscope.cn
 * 创建时间：2023/2/9 10:14:23
 * 版本：V1.0.0
 * 描述：
 *
 * ----------------------------------------------------------------
 * 修改人：
 * 时间：
 * 修改说明：
 *
 * 版本：V1.0.1
 *----------------------------------------------------------------*/
#endregion << 版 本 注 释 >>

using System;
using System.Collections.Generic;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ImageK.Java;
using ImageK.Gui;
using ImageK.Measure;
using ImageK.Plugin;
using ImageK.Plugin.Filter;
using ImageK.Util;

namespace ImageK.Process
{
    public abstract class ImageProcessor
    {
        /** Value of pixels included in masks. */
        public const uint BLACK = 0xFF000000;

        /** Value returned by getMinThreshold() when thresholding is not enabled. */
        public const double NO_THRESHOLD = -808080.0;

        /** Left justify text. */
        public const int LEFT_JUSTIFY = 0;
        /** Center justify text. */
        public const int CENTER_JUSTIFY = 1;
        /** Right justify text. */
        public const int RIGHT_JUSTIFY = 2;

        /** Isodata thresholding method */
        public const int ISODATA = 0;

        /** Modified isodata method used in Image/Adjust/Threshold tool */
        public const int ISODATA2 = 1;

        /** Composite image projection modes. */
        public const int UPDATE_RED = 1, UPDATE_GREEN = 2, UPDATE_BLUE = 3, SET_FIRST_CHANNEL = 4,
            SUM_PROJECTION = 5, MAX_PROJECTION = 6, MIN_PROJECTION = 7, INVERT_PROJECTION = 8;

        /** Interpolation methods */
        public const int NEAREST_NEIGHBOR = 0, NONE = 0, BILINEAR = 1, BICUBIC = 2;
        public const int BLUR_MORE = 0, FIND_EDGES = 1, MEDIAN_FILTER = 2, MIN = 3, MAX = 4, CONVOLVE = 5;
        public const int RED_LUT = 0, BLACK_AND_WHITE_LUT = 1, NO_LUT_UPDATE = 2, OVER_UNDER_LUT = 3;
        public const int INVERT = 0, FILL = 1, ADD = 2, MULT = 3, AND = 4, OR = 5,
            XOR = 6, GAMMA = 7, LOG = 8, MINIMUM = 9, MAXIMUM = 10, SQR = 11, SQRT = 12, EXP = 13, ABS = 14, SET = 15;

        protected const string WRONG_LENGTH = "width*height!=pixels.length";

        protected int fgColor = 0;
        protected int lineWidth = 1;
        protected int cx, cy; //current drawing coordinates
        protected Font font = IJ.font12;
        protected FontMetrics fontMetrics;
        protected bool antialiasedText;
        protected bool boldFont;
        private static String[] interpolationMethods;
        // Over/Under tresholding colors
        private static int overRed, overGreen = 255, overBlue;
        private static int underRed, underGreen, underBlue = 255;
        private static bool useBicubic;
        private int sliceNumber;
        private Overlay overlay;
        private bool noReset;


        Gui.ProgressBar progressBar;
        protected int width, snapshotWidth;
        protected int height, snapshotHeight;
        protected int roiX, roiY, roiWidth, roiHeight;
        protected int xMin, xMax, yMin, yMax;
        protected bool snapshotCopyMode;
        protected ImageProcessor mask;

        protected ColorModel baseCM; // base color model
        protected ColorModel cm;

        protected byte[] rLUT1, gLUT1, bLUT1; // base LUT
        protected byte[] rLUT2, gLUT2, bLUT2; // LUT as modified by setMinAndMax and setThreshold
        protected bool interpolate;  // replaced by interpolationMethod
        protected int interpolationMethod = NONE;
        protected double minThreshold = NO_THRESHOLD, maxThreshold = NO_THRESHOLD;
        protected int histogramSize = 256;
        protected double histogramMin, histogramMax;

        protected float[] cTable;

        protected bool lutAnimation;
        // protected MemoryImageSource source; //unused
        protected Image img;

        protected bool newPixels; // unused
        protected Color drawingColor = Color.Black;

        protected int clipXMin, clipXMax, clipYMin, clipYMax; // clip rect used by drawTo, drawLine, drawDot and drawPixel
        protected int justification = LEFT_JUSTIFY;

        protected int lutUpdateMode;
        protected WritableRaster raster;
        protected Image image;
        protected Bitmap fmImage;
        protected Graphics fmGraphics;
        protected ColorModel cm2;
        // protected SampleModel sampleModel;

        protected static IndexColorModel defaultColorModel;

        protected bool MinMaxSet;
        protected static double seed = Double.NaN;
        protected static Random rnd;

        protected bool _fillValueSet;


        protected bool inversionTested = false;
        protected bool invertedLut;

        protected void showProgress(double percentDone)
        {
            if (progressBar != null)
                progressBar.show(percentDone);
        }

        /** Assigns a progress bar to this processor. Set 'pb' to
        null to disable the progress bar. */
        public void setProgressBar(Gui.ProgressBar pb)
        {
            progressBar = pb;
        }

        #region Interpolate
        /** This method has been replaced by setInterpolationMethod(). */
        public void setInterpolate(bool interpolate)
        {
            this.interpolate = interpolate;
            if (interpolate)
                interpolationMethod = useBicubic ? BICUBIC : BILINEAR;
            else
                interpolationMethod = NONE;
        }

        /** Use this method to set the interpolation method (NONE,
        BILINEAR or BICUBIC) used by scale(), resize() and rotate(). */
        public void setInterpolationMethod(int method)
        {
            if (method < NONE || method > BICUBIC)
                throw new ArgumentException("Invalid interpolation method");
            interpolationMethod = method;
            interpolate = method != NONE ? true : false;
        }

        /** Returns the current interpolation method (NONE, BILINEAR or BICUBIC). */
        public int getInterpolationMethod()
        {
            return interpolationMethod;
        }

        public static string[] getInterpolationMethods()
        {
            if (interpolationMethods == null)
                interpolationMethods = new string[] { "None", "Bilinear", "Bicubic" };
            return interpolationMethods;
        }

        /** Returns the value of the interpolate field. */
        public bool getInterpolate()
        {
            return interpolate;
        }
        #endregion

        #region ColorModel
        protected void makeDefaultColorModel()
        {
            cm = getDefaultColorModel();
        }

        /** Inverts the values in this image's LUT (indexed color model).
        Does nothing if this is a ColorProcessor. */
        public virtual void invertLut()
        {
            IndexColorModel icm = (IndexColorModel)getColorModel();
            int mapSize = icm.getMapSize();
            byte[] reds = new byte[mapSize];
            byte[] greens = new byte[mapSize];
            byte[] blues = new byte[mapSize];
            byte[] reds2 = new byte[mapSize];
            byte[] greens2 = new byte[mapSize];
            byte[] blues2 = new byte[mapSize];
            icm.getReds(reds);
            icm.getGreens(greens);
            icm.getBlues(blues);
            for (int i = 0; i < mapSize; i++)
            {
                reds2[i] = (byte)(reds[mapSize - i - 1] & 255);
                greens2[i] = (byte)(greens[mapSize - i - 1] & 255);
                blues2[i] = (byte)(blues[mapSize - i - 1] & 255);
            }
            ColorModel cm = new IndexColorModel(8, mapSize, reds2, greens2, blues2);
            double min = getMin(), max = getMax();
            setColorModel(cm);
            setMinAndMax(min, max);
        }

        /** Returns the LUT index that's the best match for this color. */
        public virtual int getBestIndex(Color c)
        {
            IndexColorModel icm;
            if (cm == null)
                makeDefaultColorModel();
            if (minThreshold != NO_THRESHOLD)
            {
                double saveMin = getMinThreshold();
                double saveMax = getMaxThreshold();
                resetThreshold();
                icm = (IndexColorModel)cm;
                setThreshold(saveMin, saveMax, lutUpdateMode);
            }
            else
                icm = (IndexColorModel)cm;
            int mapSize = icm.getMapSize();
            byte[] rLUT = new byte[mapSize];
            byte[] gLUT = new byte[mapSize];
            byte[] bLUT = new byte[mapSize];
            icm.getReds(rLUT);
            icm.getGreens(gLUT);
            icm.getBlues(bLUT);
            int minDistance = int.MaxValue;
            int distance;
            int minIndex = 0;
            int r1 = c.R;
            int g1 = c.G;
            int b1 = c.B;
            if (!(r1 == g1 && g1 == b1 && r1 == b1) && icm == defaultColorModel)
            {
                double[] w = ColorProcessor.getWeightingFactors();
                r1 = (int)Math.Round(3 * r1 * w[0]);
                g1 = (int)Math.Round(3 * g1 * w[1]);
                b1 = (int)Math.Round(3 * b1 * w[2]);
            }
            int r2, b2, g2;
            for (int i = 0; i < mapSize; i++)
            {
                r2 = rLUT[i] & 0xff; g2 = gLUT[i] & 0xff; b2 = bLUT[i] & 0xff;
                distance = (r2 - r1) * (r2 - r1) + (g2 - g1) * (g2 - g1) + (b2 - b1) * (b2 - b1);
                //ij.IJ.log("  "+i+" "+minIndex+" "+distance+"   "+(rLUT[i]&255)+" "+(gLUT[i]&255)+" "+(bLUT[i]&255));
                if (distance < minDistance)
                {
                    minDistance = distance;
                    minIndex = i;
                }
                if (minDistance == 0.0)
                    break;
            }
            return minIndex;
        }


        /** Returns this processor's color model. For non-RGB processors,
        this is the base lookup table (LUT), not the one that may have
        been modified by setMinAndMax() or setThreshold(). */
        public ColorModel getColorModel()
        {
            if (cm == null)
                makeDefaultColorModel();
            if (baseCM != null)
                return baseCM;
            else
                return cm;
        }

        private IndexColorModel getIndexColorModel()
        {
            ColorModel cm2 = baseCM;
            if (cm2 == null)
                cm2 = cm;
            if (cm2 != null && (cm2 is IndexColorModel))
            return (IndexColorModel)cm2;

            else
            return null;
        }

        /** Returns the current color model, which may have
        been modified by setMinAndMax() or setThreshold(). */
        public ColorModel getCurrentColorModel()
        {
            if (cm == null) makeDefaultColorModel();
            return cm;
        }

        /** Sets the color model. Must be an IndexColorModel (aka LUT)
        for all processors except the ColorProcessor. */
        public virtual void setColorModel(ColorModel cm)
        {
            if (cm != null && !(cm is IndexColorModel))
                throw new ArgumentException("IndexColorModel required");
            if (cm != null && cm is LUT)
                cm = ((LUT)cm).getColorModel();
            this.cm = cm;
            baseCM = null;
            rLUT1 = rLUT2 = null;
            inversionTested = false;
            minThreshold = NO_THRESHOLD;
        }
        #endregion

        #region Lut
        public LUT getLut()
        {
            ColorModel cm2 = getColorModel();
            if (cm2 != null && (cm2 is IndexColorModel))
                return new LUT((IndexColorModel)cm2, getMin(), getMax());

            else
                return null;
        }

        public void setLut(LUT lut)
        {
            if (lut == null)
                setColorModel(null);
            else
            {
                setColorModel(lut.getColorModel());
                if (lut.min != 0.0 || lut.max != 0.0)
                    setMinAndMax(lut.min, lut.max);
            }
        }
        #endregion

        /** Sets the global (Color Picker) foreground color
         * as the fill/draw color.
         * @see ij.gui.Toolbar#setForegroundColor(Color)
         * @see ij.gui.Toolbar#setForegroundValue(double)
        */
        public void setGlobalForegroundColor()
        {
            double value = ToolBar.getForegroundValue();
            if (double.IsNaN(value))
                setColor(ToolBar.getForegroundColor());
            else
                setValue(value);
        }

        /** Sets the global (Color Picker) background color
         * as the fill/draw color.
         * @see ij.gui.Toolbar#setBackgroundColor(Color)
         * @see ij.gui.Toolbar#setBackgroundValue(double)
        */
        public void setGlobalBackgroundColor()
        {
            double value = ToolBar.getBackgroundValue();
            if (double.IsNaN(value))
                setColor(ToolBar.getBackgroundColor());
            else
                setValue(value);
        }
        /** Returns the smallest displayed pixel value. */
        public abstract double getMin();

        /** Returns the largest displayed pixel value. */
        public abstract double getMax();

        /** This image will be displayed by mapping pixel values in the
         * range min-max to screen values in the range 0-255. For
         * byte images, this mapping is done by updating the LUT. For
         * short and float images, it's done by generating 8-bit AWT
         * images. For RGB images, it's done by changing the pixel values.
         * With signed 16-bit images, use IJ.setMinAndMax(imp,min,max).
         * @see ij.IJ#setMinAndMax(ij.ImagePlus,double,double)
        */
        public abstract void setMinAndMax(double min, double max);

        /** For short and float images, recalculates the min and max
        image values needed to correctly display the image. For
        ByteProcessors, resets the LUT. */
        public virtual void resetMinAndMax() { }

        /** Returns a copy of this image is the form of an AWT Image. */
        public abstract Image createImage();

        /** Makes a copy of this image's pixel data that can be
            later restored using reset() or reset(mask).
            @see ImageProcessor#reset
            @see ImageProcessor#reset(ImageProcessor)
        */
        public abstract void snapshot();

        /** Restores the pixel data from the snapshot (undo) buffer. */
        public abstract void reset();

        /** Swaps the pixel and snapshot (undo) buffers. */
        public abstract void swapPixelArrays();

        /** Restores pixels from the snapshot buffer that are
        within the rectangular roi but not part of the mask. */
        public abstract void reset(ImageProcessor mask);

        /** Returns a new, blank processor with the specified width and height. */
        public abstract ImageProcessor createProcessor(int width, int height);

        /** Returns 'true' if the fill/draw value has been set. */
        public bool fillValueSet()
        {
            return _fillValueSet;
        }

        /** Sets the background fill value used by the rotate() and scale() methods. */
        public abstract void setBackgroundValue(double value);

        /** Returns the background fill value. */
        public abstract double getBackgroundValue();

        #region ROI
        /** Defines a rectangular region of interest and sets the mask
            to null if this ROI is not the same size as the previous one.
            @see ImageProcessor#resetRoi
        */
        public void setRoi(Rectangle roi)
        {
            if (roi == null)
                resetRoi();
            else
                setRoi(roi.X, roi.Y, roi.Width, roi.Height);
        }

        /** Defines a rectangular region of interest and sets the mask to
            null if this ROI is not the same size as the previous one.
            @see ImageProcessor#resetRoi
        */
        public void setRoi(int x, int y, int rwidth, int rheight)
        {
            if (x < 0 || y < 0 || x + rwidth > width || y + rheight > height)
            {
                //find intersection of roi and this image
                Rectangle r1 = new Rectangle(x, y, rwidth, rheight);
                Rectangle r2 = new Rectangle(r1.X,r1.Y,r1.Width,r1.Height);
                r2.Intersect(new Rectangle(0, 0, width, height));
                if (r2.Width <= 0 || r2.Height <= 0)
                {
                    roiX = 0; roiY = 0; roiWidth = 0; roiHeight = 0;
                    xMin = 0; xMax = 0; yMin = 0; yMax = 0;
                    mask = null;
                    return;
                }
                if (mask != null && mask.getWidth() == rwidth && mask.getHeight() == rheight)
                {
                    Rectangle r3 = new Rectangle(0, 0, r2.Width, r2.Height);
                    if (x < 0) r3.X = -x;
                    if (y < 0) r3.Y = -y;
                    mask.setRoi(r3);
                    if (mask != null)
                    {
                        //todo:
                        // mask = mask.crop();
                    }
                }
                roiX = r2.X; roiY = r2.Y; roiWidth = r2.Width; roiHeight = r2.Height;
            }
            else
            {
                roiX = x; roiY = y; roiWidth = rwidth; roiHeight = rheight;
            }
            if (mask != null && (mask.getWidth() != roiWidth || mask.getHeight() != roiHeight))
                mask = null;
            //setup limits for 3x3 filters
            xMin = Math.Max(roiX, 1);
            xMax = Math.Min(roiX + roiWidth - 1, width - 2);
            yMin = Math.Max(roiY, 1);
            yMax = Math.Min(roiY + roiHeight - 1, height - 2);
        }

        /** Defines a non-rectangular region of interest that will consist of a
            rectangular ROI and a mask. After processing, call <code>reset(mask)</code>
            to restore non-masked pixels. Here is an example:
            <pre>
            ip.setRoi(new OvalRoi(50, 50, 100, 50));
            ip.fill();
            ip.reset(ip.getMask());
            </pre>
            The example assumes <code>snapshot()</code> has been called, which is the case
            for code executed in the <code>run()</code> method of plugins that implement the
            <code>PlugInFilter</code> interface.
            @see ij.ImagePlus#getRoi
        */
        public void setRoi(Roi roi)
        {
            //todo:
            if (roi == null)
                resetRoi();
            else
            {
                // if ((roi is PointRoi) && roi.size() == 1) {
                //     setMask(null);
                //     Polygon p = roi.getPolygon();
                //     setRoi(p.xpoints[0], p.ypoints[0], 1, 1);
                // } 
                // else
                {
                    setMask(roi.getMask());
                    setRoi(roi.getBounds());
                }
            }
        }

        /** Defines a polygonal region of interest that will consist of a
            rectangular ROI and a mask. After processing, call <code>reset(mask)</code>
            to restore non-masked pixels. Here is an example:
            <pre>
            Polygon p = new Polygon();
            p.addPoint(50, 0); p.addPoint(100, 100); p.addPoint(0, 100);
            ip.setRoi(triangle);
            ip.invert();
            ip.reset(ip.getMask());
            </pre>
            The example assumes <code>snapshot()</code> has been called, which is the case
            for code executed in the <code>run()</code> method of plugins that implement the
            <code>PlugInFilter</code> interface.
            @see ij.gui.Roi#getPolygon
            @see ImageProcessor#drawPolygon
            @see ImageProcessor#fillPolygon
        */
        public void setRoi(Polygon roi)
        {
            if (roi==null)
            { resetRoi(); return; }
            Rectangle bounds = roi.getBounds();
            for (int i = 0; i<roi.npoints; i++)
            {
                roi.xpoints[i] -= bounds.X;
                roi.ypoints[i] -= bounds.Y;
            }
            PolygonFiller pf = new PolygonFiller();
            pf.setPolygon(roi.xpoints, roi.ypoints, roi.npoints);
            ImageProcessor mask = pf.getMask(bounds.Width, bounds.Height);
            setMask(mask);
            setRoi(bounds);
            for (int i = 0; i<roi.npoints; i++)
            {
                roi.xpoints[i] += bounds.X;
                roi.ypoints[i] += bounds.Y;
            }
        }

        /** Returns a Rectangle that represents the current
        region of interest. */
        public Rectangle getRoi()
        {
            return new Rectangle(roiX, roiY, roiWidth, roiHeight);
        }

        /** Sets the ROI (Region of Interest) and clipping rectangle to the entire image. */
        public void resetRoi()
        {
            roiX = 0; roiY = 0; roiWidth = width; roiHeight = height;
            xMin = 1; xMax = width - 2; yMin = 1; yMax = height - 2;
            mask = null;
            clipXMin = 0; clipXMax = width - 1; clipYMin = 0; clipYMax = height - 1;
        }
        #endregion

        #region Mask

        /** Defines a byte mask that limits processing to an
        irregular ROI. Background pixels in the mask have
        a value of zero. */
        public void setMask(ImageProcessor mask)
        {
            this.mask = mask;
        }

        /** For images with irregular ROIs, returns a mask, otherwise,
        returns null. Pixels outside the mask have a value of zero. */
        public ImageProcessor getMask()
        {
            return mask;
        }


        /** Returns a reference to the mask pixel array, or null if there is no mask. */
        public byte[] getMaskArray()
        {
            return mask != null ? (byte[])mask.getPixels() : null;
        }
        #endregion

        #region Threshold

        /** Sets the lower and upper threshold levels. The 'lutUpdate' argument
        can be RED_LUT, BLACK_AND_WHITE_LUT, OVER_UNDER_LUT or NO_LUT_UPDATE.
        Thresholding of RGB images is not supported. */
        public virtual void setThreshold(double minThreshold, double maxThreshold, int lutUpdate)
        {
            //ij.IJ.log("setThreshold: "+" "+minThreshold+" "+maxThreshold+" "+lutUpdate);
            if (this is ColorProcessor)
			    return;
            this.minThreshold = minThreshold;
            this.maxThreshold = maxThreshold;
            lutUpdateMode = lutUpdate;
            if (minThreshold == NO_THRESHOLD)
            {
                resetThreshold();
                return;
            }
            if (lutUpdate == NO_LUT_UPDATE)
                return;
            if (rLUT1 == null)
            {
                if (cm == null)
                    makeDefaultColorModel();
                baseCM = cm;
                IndexColorModel m = (IndexColorModel)cm;
                rLUT1 = new byte[256]; gLUT1 = new byte[256]; bLUT1 = new byte[256];
                m.getReds(rLUT1); m.getGreens(gLUT1); m.getBlues(bLUT1);
                rLUT2 = new byte[256]; gLUT2 = new byte[256]; bLUT2 = new byte[256];
            }
            int t1 = (int)minThreshold;
            int t2 = (int)maxThreshold;
            int index;
            if (lutUpdate == RED_LUT)
                for (int i = 0; i < 256; i++)
                {
                    if (i >= t1 && i <= t2)
                    {
                        rLUT2[i] = (byte)255;
                        gLUT2[i] = (byte)0;
                        bLUT2[i] = (byte)0;
                    }
                    else
                    {
                        rLUT2[i] = rLUT1[i];
                        gLUT2[i] = gLUT1[i];
                        bLUT2[i] = bLUT1[i];
                    }
                }
            else if (lutUpdate == BLACK_AND_WHITE_LUT)
            {
                // updated in v1.43i by Gabriel Lindini to use blackBackground setting
                byte foreground = Prefs.blackBackground ? (byte)255 : (byte)0;
                byte background = (byte)(255 - foreground);
                for (int i = 0; i < 256; i++)
                {
                    if (i >= t1 && i <= t2)
                    {
                        rLUT2[i] = foreground;
                        gLUT2[i] = foreground;
                        bLUT2[i] = foreground;
                    }
                    else
                    {
                        rLUT2[i] = background;
                        gLUT2[i] = background;
                        bLUT2[i] = background;
                    }
                }
            }
            else
            {
                for (int i = 0; i < 256; i++)
                {
                    if (i >= t1 && i <= t2)
                    {
                        rLUT2[i] = rLUT1[i];
                        gLUT2[i] = gLUT1[i];
                        bLUT2[i] = bLUT1[i];
                    }
                    else if (i > t2)
                    {
                        rLUT2[i] = (byte)overRed;
                        gLUT2[i] = (byte)overGreen;
                        bLUT2[i] = (byte)overBlue;
                    }
                    else
                    {
                        rLUT2[i] = (byte)underRed;
                        gLUT2[i] = (byte)underGreen;
                        bLUT2[i] = (byte)underBlue;
                    }
                }
            }
            cm = new IndexColorModel(8, 256, rLUT2, gLUT2, bLUT2);
        }

        /** Automatically sets the lower and upper threshold levels, where 'method'
         * must be "Default", "Huang", "Intermodes", "IsoData", "IJ_IsoData", "Li",
         * "MaxEntropy", "Mean", "MinError", "Minimum", "Moments", "Otsu",
         * "Percentile", "RenyiEntropy", "Shanbhag", "Triangle" or "Yen". The
         * 'method' string may also include the keywords 'dark' (dark background)
         * 'red' (red LUT, the default), 'b&w' (black and white LUT), 'over/under' (over/under LUT) or
         * 'no-lut' (no LUT changes), for example "Huang dark b&w". The display range
         * of 16-bit and 32-bit images is not reset if the 'method' string contains 'no-reset'.
         * @see ImageProcessor#resetThreshold
         * @see ImageProcessor#setThreshold
         * @see ImageProcessor#createMask
        */
        public void setAutoThreshold(String method)
        {
            if (method == null)
                throw new ArgumentException("Null method");
            bool darkBackground = method.Contains("dark");
            noReset = method.Contains("no-reset");
            int lut = RED_LUT;
            if (method.Contains("b&w"))
                lut = BLACK_AND_WHITE_LUT;
            if (method.Contains("over"))
                lut = OVER_UNDER_LUT;
            if (method.Contains("no-lut"))
                lut = NO_LUT_UPDATE;
            int index = method.IndexOf(" ");
            if (index != -1)
                method = method.Substring(0, index);
            setAutoThreshold(method, darkBackground, lut);
            noReset = false;
        }

        public void setAutoThreshold(string mString, bool darkBackground, int lutUpdate)
        {
            AutoThresholder.Method m;
            if (!Enum.TryParse<AutoThresholder.Method>(mString, out m))
            {
                throw new ArgumentException("Invalid method (\"" + mString + "\")");
            }
            else
            {
                setAutoThreshold(m, darkBackground, lutUpdate);
            }
            
        }

        public void setAutoThreshold(AutoThresholder.Method method, bool darkBackground)
        {
            setAutoThreshold(method, darkBackground, RED_LUT);
        }

        public void setAutoThreshold(AutoThresholder.Method method, bool darkBackground, int lutUpdate)
        {
            if (method == null || (this is ColorProcessor))
                return;
            double min = 0.0, max = 0.0;
            bool notByteData = !(this is ByteProcessor);
            ImageProcessor ip2 = this;
            if (notByteData)
            {
                ImageProcessor mask = ip2.getMask();
                Rectangle rect = ip2.getRoi();
                if (!noReset || lutUpdate == OVER_UNDER_LUT)
                    ip2.resetMinAndMax();
                noReset = false;
                min = ip2.getMin(); max = ip2.getMax();
                ip2 = ip2.convertToByte(true);
                ip2.setMask(mask);
                ip2.setRoi(rect);
            }
            ImageStatistics stats = ip2.getStats();
            AutoThresholder thresholder = new AutoThresholder();
            int threshold = thresholder.getThreshold(method, stats.Histogram);
            double lower, upper;
            if (darkBackground)
            {
                if (isInvertedLut())
                { lower = 0.0; upper = threshold; }
                else
                { lower = threshold + 1; upper = 255.0; }
            }
            else
            {
                if (isInvertedLut())
                { lower = threshold + 1; upper = 255.0; }
                else
                { lower = 0.0; upper = threshold; }
            }
            if (lower > 255) lower = 255;
            scaleAndSetThreshold(lower, upper, lutUpdate);
        }

        /** Automatically sets the lower and upper threshold levels, where 'method'
             must be ISODATA or ISODATA2 and 'lutUpdate' must be RED_LUT,
             BLACK_AND_WHITE_LUT, OVER_UNDER_LUT or NO_LUT_UPDATE.
        */
        public void setAutoThreshold(int method, int lutUpdate)
        {
            if (method < 0 || method > ISODATA2)
                throw new ArgumentException("Invalid thresholding method");
            if (this is ColorProcessor)
			    return;
            bool notByteData = !(this is ByteProcessor);
            ImageProcessor ip2 = this;
            if (notByteData)
            {
                ImageProcessor mask = ip2.getMask();
                Rectangle rect = ip2.getRoi();
                resetMinAndMax();
                ip2 = convertToByte(true);
                ip2.setMask(mask);
                ip2.setRoi(rect);
            }
            ImageStatistics stats = ip2.getStats();
            int[] histogram = stats.Histogram;
            int originalModeCount = histogram[stats.mode];
            if (method == ISODATA2)
            {
                int maxCount2 = 0;
                for (int i = 0; i < stats.nBins; i++)
                {
                    if ((histogram[i] > maxCount2) && (i != stats.mode))
                        maxCount2 = histogram[i];
                }
                int hmax = stats.maxCount;
                if ((hmax > (maxCount2 * 2)) && (maxCount2 != 0))
                {
                    hmax = (int)(maxCount2 * 1.5);
                    histogram[stats.mode] = hmax;
                }
            }
            int threshold = ip2.getAutoThreshold(stats.Histogram);
            histogram[stats.mode] = originalModeCount;
            float[] hist = new float[256];
            for (int i = 0; i < 256; i++)
                hist[i] = stats.Histogram[i];
            FloatProcessor fp = new FloatProcessor(256, 1, hist, null);
            GaussianBlur gb = new GaussianBlur();
            gb.blur1Direction(fp, 2.0, 0.01, true, 0);
            float maxCount = 0f, sum = 0f, mean, count;
            int mode = 0;
            for (int i = 0; i < 256; i++)
            {
                count = hist[i];
                sum += count;
                if (count > maxCount)
                {
                    maxCount = count;
                    mode = i;
                }
            }
            double avg = sum / 256.0;
            double lower, upper;
            if (maxCount / avg > 1.5)
            {
                if ((stats.max - mode) > (mode - stats.min))
                { lower = threshold; upper = 255.0; }
                else
                { lower = 0.0; upper = threshold; }
            }
            else
            {
                if (isInvertedLut())
                { lower = threshold; upper = 255.0; }
                else
                { lower = 0.0; upper = threshold; }
            }
            scaleAndSetThreshold(lower, upper, lutUpdate);

        }

        /** Set the threshold using a 0-255 range. For 16-bit and 32-bit images,
         *  this range is taken as relative with respect to the range between the
         *  current display min and max, but lower=0 and upper=255 are set to the
         * full-range limits of 16-bit images and -/+1e30 for float images.
        */
        public void scaleAndSetThreshold(double lower, double upper, int lutUpdate)
        {
            int bitDepth = getBitDepth();
            if (bitDepth != 8 && lower != NO_THRESHOLD)
            {
                double min = getMin();
                double max = getMax();
                if (max > min)
                {
                    if (lower == 0.0)
                    {
                        if (bitDepth == 32)
                            lower = Math.Min(min, -1e30); // can't set to -Float.MAX_VALUE; causes FloodFiller.particleAnalyzerFill to hang;
                    }
                    else
                        lower = min + (lower / 255.0) * (max - min);
                    if (upper == 255.0)
                    {
                        if (bitDepth == 16)
                            upper = 65535;
                        else if (bitDepth == 32)
                            upper = Math.Max(max, 1e30);
                    }
                    else
                        upper = min + (upper / 255.0) * (max - min);
                }
                else
                    lower = upper = min;
            }
            setThreshold(lower, upper, lutUpdate);
        }

        /** Disables thresholding. */
        public void resetThreshold()
        {
            minThreshold = NO_THRESHOLD;
            if (baseCM != null)
            {
                cm = baseCM;
                baseCM = null;
            }
            rLUT1 = rLUT2 = null;
            inversionTested = false;
        }

        /** Returns the lower threshold level. Returns NO_THRESHOLD
            if thresholding is not enabled. */
        public double getMinThreshold()
        {
            return minThreshold;
        }

        /** Returns the upper threshold level. */
        public double getMaxThreshold()
        {
            return maxThreshold;
        }

        /** Returns the LUT update mode, which can be RED_LUT, BLACK_AND_WHITE_LUT,
        OVER_UNDER_LUT or NO_LUT_UPDATE. */
        public int getLutUpdateMode()
        {
            return lutUpdateMode;
        }

        /* Sets the threshold levels (non-visible) of an 8-bit mask based on
            the state of Prefs.blackBackground and isInvertedLut().
            @see ImageProcessor#resetBinaryThreshold
        */
        public void setBinaryThreshold()
        {
            //ij.IJ.log("setMaskThreshold1");
            if (!(this is ByteProcessor)) return;
            double t1 = 255.0, t2 = 255.0;
            bool invertedLut = isInvertedLut();
            if ((invertedLut && Prefs.blackBackground) || (!invertedLut && !Prefs.blackBackground))
            {
                t1 = 0.0;
                t2 = 0.0;
            }
            //ij.IJ.log("setMaskThreshold2 "+t1+" "+t2);
            setThreshold(t1, t2, ImageProcessor.NO_LUT_UPDATE);
        }

        /** Resets the threshold if minThreshold=maxThreshold and lutUpdateMode=NO_LUT_UPDATE.
            This removes the invisible threshold set by the MakeBinary and Convert to Mask commands.
            @see ImageProcessor#setBinaryThreshold
        */
        public void resetBinaryThreshold()
        {
            if (minThreshold == maxThreshold && lutUpdateMode == NO_LUT_UPDATE)
                resetThreshold();
        }
        #endregion

        /** Draws the specified ROI on this image using the stroke
            width, stroke color and fill color defined by roi.setStrokeWidth,
            roi.setStrokeColor() and roi.setFillColor(). Works   with RGB
            images. Does not work with 16-bit and float images.
            @see ImageProcessor#draw
            @see ImageProcessor#drawOverlay
        */
        public void drawRoi(Roi roi)
        {
            Image img = createImage();
            Graphics g = Graphics.FromImage(img);
            ImagePlus imp = roi.getImage();
            if (imp!=null)
            {
                roi.setImage(null);
                roi.drawOverlay(g);
                roi.setImage(imp);
            }
            else
                roi.drawOverlay(g);
        }

        /** Draws the specified Overlay on this image. Works best
            with RGB images. Does not work with 16-bit and float
            images. Requires Java 1.6.
            @see ImageProcessor#drawRoi
        */
        public void drawOverlay(Overlay overlay)
        {
            Roi[] rois = overlay.toArray();
            for (int i = 0; i<rois.Length; i++)
                drawRoi(rois[i]);
        }

        /** Set a lookup table used by getPixelValue(), getLine() and
            convertToFloat() to calibrate pixel values. The length of
            the table must be 256 for byte images and 65536 for short
            images. RGB and float processors do not do calibration.
            @see ij.measure.Calibration#setCTable
        */
        public void setCalibrationTable(float[] cTable)
        {
            this.cTable = cTable;
        }

        /** Returns the calibration table or null. */
        public float[] getCalibrationTable()
        {
            return cTable;
        }


        /** Sets a new pixel array for the snapshot (undo) buffer. */
        public abstract void setSnapshotPixels(object pixels);

        /** Returns a reference to the snapshot (undo) buffer, or null. */
        public abstract object getSnapshotPixels();

        /** Returns the value of the pixel at (x,y). For byte and short
         * images, returns a calibrated value if a calibration table
         * has been set using setCalibraionTable(). For RGB images,
         * returns the luminance value.
         * @see ImageProcessor#getPixel
         * @see ImageProcessor#getValue
         * @see ImageProcessor#getf
        */
        public abstract float getPixelValue(int x, int y);

        /** Stores the specified value at (x,y). */
        public abstract void putPixelValue(int x, int y, double value);

        /** Sets the pixel at (x,y) to the current fill/draw value. */
        public abstract void drawPixel(int x, int y);

        /** Sets a new pixel array for the image. The length of the array must be equal to width*height.
        Use setSnapshotPixels(null) to clear the snapshot buffer. */
        public abstract void setPixels(object pixels);

        protected void resetPixels(object pixels)
        {
            if (pixels == null)
            {
                if (img != null)
                {
                    img.Dispose();
                    img = null;
                }
            }
        }

        /** Copies the image contained in 'ip' to (xloc, yloc) using one of
        the transfer modes defined in the Blitter interface. */
        public abstract void copyBits(ImageProcessor ip, int xloc, int yloc, int mode);

        /** Returns the width of this image in pixels. */
        public int getWidth()
        {
            return width;
        }

        /** Returns the height of this image in pixels. */
        public int getHeight()
        {
            return height;
        }

        /** Returns the bit depth, 8, 16, 24 (RGB) or 32. RGB images actually use 32 bits per pixel. */
        public virtual int getBitDepth()
        {
            object pixels = getPixels();
            if (pixels == null)
                return 0;
            else if (pixels is byte[])
                return 8;
            else if (pixels is short[])
                return 16;
            else if (pixels is int[])
                return 24;
            else if (pixels is float[])
                return 32;
            else
                return 0;
        }

        /** Returns the default grayscale IndexColorModel. */
        public IndexColorModel getDefaultColorModel()
        {
            if (defaultColorModel == null)
            {
                byte[] r = new byte[256];
                byte[] g = new byte[256];
                byte[] b = new byte[256];
                for (int i = 0; i < 256; i++)
                {
                    r[i] = (byte)i;
                    g[i] = (byte)i;
                    b[i] = (byte)i;
                }
                defaultColorModel = new IndexColorModel(8, 256, r, g, b);
            }
            return defaultColorModel;
        }

        #region Lut
        /** Returns true if this image uses an inverting LUT
        that displays zero as white and 255 as black. */
        public virtual bool isInvertedLut()
        {
            //todo:
            return false;

            // if (inversionTested)
            //     return invertedLut;
            // IndexColorModel icm = getIndexColorModel();
            // if (icm == null)
            //     return false;
            // bool hasAscendingStep = false;
            // int v1, v2;
            // for (int i = 1; i < 255; i++)
            // {
            //     v1 = icm.getRed(i - 1) + icm.getGreen(i - 1) + icm.getBlue(i - 1);
            //     v2 = icm.getRed(i) + icm.getGreen(i) + icm.getBlue(i);
            //     if (v1 < v2)
            //     {
            //         hasAscendingStep = true;
            //         break;
            //     }
            // }
            // invertedLut = !hasAscendingStep;
            // inversionTested = true;
            // return invertedLut;
        }

        /** Returns true if this image uses a color LUT. */
        public bool isColorLut()
        {
            //todo:
            return false;
            // IndexColorModel icm = getIndexColorModel();
            // if (icm == null)
            //     return false;
            // int mapSize = icm.getMapSize();
            // byte[] reds = new byte[mapSize];
            // byte[] greens = new byte[mapSize];
            // byte[] blues = new byte[mapSize];
            // icm.getReds(reds);
            // icm.getGreens(greens);
            // icm.getBlues(blues);
            // boolean isColor = false;
            // for (int i = 0; i < mapSize; i++)
            // {
            //     if ((reds[i] != greens[i]) || (greens[i] != blues[i]))
            //     {
            //         isColor = true;
            //         break;
            //     }
            // }
            // return isColor;
        }

        /** Returns true if this image uses a pseudocolor or grayscale LUT,
        in other words, is this an image that can be filtered. */
        public bool isPseudoColorLut()
        {
            IndexColorModel icm = getIndexColorModel();
            if (icm==null)
                return false;
            if (getMinThreshold()!=NO_THRESHOLD)
                return true;
            int mapSize = icm.getMapSize();
            if (mapSize!=256)
                return false;
            byte[] reds = new byte[mapSize];
            byte[] greens = new byte[mapSize];
            byte[] blues = new byte[mapSize];
            icm.getReds(reds);
            icm.getGreens(greens);
            icm.getBlues(blues);
            int r, g, b, d;
            int r2 = reds[0]&255, g2 = greens[0]&255, b2 = blues[0]&255;
            double sum = 0.0, sum2 = 0.0;
            for (int i = 0; i<mapSize; i++)
            {
                r=reds[i]&255; g=greens[i]&255; b=blues[i]&255;
                d=r-r2; sum+=d; sum2+=d*d;
                d=g-g2; sum+=d; sum2+=d*d;
                d=b-b2; sum+=d; sum2+=d*d;
                r2=r; g2=g; b2=b;
            }
            double stdDev = (768*sum2-sum*sum)/768.0;
            if (stdDev>0.0)
                stdDev = Math.Sqrt(stdDev/(767.0));
            else
                stdDev = 0.0;
            bool isPseudoColor = stdDev<20.0;
            if ((int)stdDev==67) isPseudoColor = true; // "3-3-2 RGB" LUT
            return isPseudoColor;
        }

        /** Returns true if the image is using the default grayscale LUT. */
        public bool isDefaultLut()
        {
            if (cm==null)
                makeDefaultColorModel();
            IndexColorModel icm = getIndexColorModel();
            if (icm==null)
                return false;
            int mapSize = icm.getMapSize();
            if (mapSize!=256)
                return false;
            byte[] reds = new byte[mapSize];
            byte[] greens = new byte[mapSize];
            byte[] blues = new byte[mapSize];
            icm.getReds(reds);
            icm.getGreens(greens);
            icm.getBlues(blues);
            bool isDefault = true;
            for (int i = 0; i<mapSize; i++)
            {
                if ((reds[i]&255)!=i || (greens[i]&255)!=i || (blues[i]&255)!=i)
                {
                    isDefault = false;
                    break;
                }
            }
            return isDefault;
        }
        #endregion

        #region Color、Font
        /** Sets the default fill/draw value to the pixel
        value closest to the specified color. */
        public abstract void setColor(Color color);

        /** Sets the fill/draw color, where 'color' is
         * "white", "black", "blue", etc., or a hex value
         * like "#ff0000".
        */
        public void setColor(string color)
        {
            //todo:
            // setColor(Colors.decode(color, Color.white));
            setColor(Colors.getColor(color, Color.White));
        }


        /** Sets the background fill/draw color. */
        public void setBackgroundColor(Color color)
        {
        }

        /** Sets the default fill/draw value. */
        public void setColor(int value)
        {
            setValue(value);
        }

        /** Sets the default fill/draw value. */
        public abstract void setValue(double value);

        /** Sets the font used by drawString(). */
        public void setFont(Font font)
        {
            //todo:
            this.font = font;
            boldFont = font.Bold;
            setupFontMetrics();
            // fmGraphics.setFont(font);
            // Java2.setAntialiasedText(fmGraphics, antialiasedText);
            // fontMetrics = fmGraphics.getFontMetrics(font);
        }

        /** Returns the current FontMetrics. */
        public FontMetrics getFontMetrics()
        {
            setupFontMetrics();
            return fontMetrics;
        }

        /** Returns the width in pixels of the specified string, including any background
         *  space (whitespace) between the x drawing coordinate and the string, not necessarily
         *  including all whitespace at the right. */
        public int getStringWidth(string s)
        {
            // Note that fontMetrics.getStringBounds often underestimates the width (worst for italic fonts on Macs)
            // On the other hand, GlyphVector.getPixelBounds (returned by this.getStringBounds)
            // does not include the full character width of e.g. the '1' character, which would make
            // lists of right-justified numbers such as the y axis of plots look ugly.
            // Thus, the maximum of both methods is returned.
            // Rectangle2D rect = getStringBounds(s);
            // return (int)Math.Max(fontMetrics.getStringBounds(s, fmGraphics).getWidth(), rect.getX() + rect.getWidth());
            //todo:
            return (int)fontMetrics.StringWidth(s);
        }

        /** Returns a rectangle enclosing the pixels affected by drawString
         *  assuming it is drawn at (x=0, y=0). As drawString draws above the drawing location,
         *  the y coordinate of the rectangle is negative. */
        public Rectangle getStringBounds(String s)
        {
            setupFontMetrics();
            // GlyphVector gv = font.createGlyphVector(fmGraphics.getFontRenderContext(), s);
            // Rectangle2D rect = gv.getPixelBounds(null, 0.f, -fontMetrics.getDescent());
            // return new Rectangle((int)rect.getX(), (int)rect.getY(), (int)rect.getWidth(), (int)rect.getHeight());
            SizeF size = fmGraphics.MeasureString(s, font);
            return new Rectangle(0, 0, (int)size.Width, (int)size.Height);
        }

        /** Returns the current font. */
        public Font getFont()
        {
            return font;
        }

        /** Specifies whether or not text is drawn using antialiasing. Antialiased
        test requires an 8 bit or RGB image. Antialiasing does not
        work with 8-bit images that are not using 0-255 display range. */
        public void setAntialiasedText(bool antialiasedText)
        {
            // setupFontMetrics();
            // if (antialiasedText && (((this is ByteProcessor)&& getMin() == 0.0 && getMax() == 255.0) || (this is ColorProcessor)))
            // this.antialiasedText = true;
            //
            // else
            // this.antialiasedText = false;
            // Java2.setAntialiasedText(fmGraphics, this.antialiasedText);
            // fontMetrics = fmGraphics.getFontMetrics(font);
        }
        #endregion

        /** This method is used to display virtual stack overlays. */
        public void setOverlay(Overlay overlay)
        {
            this.overlay = overlay;
        }

        public Overlay getOverlay()
        {
            return overlay;
        }

        /**	The getPixelsCopy() method returns a reference to the
            snapshot buffer if it is not null and 'snapshotCopyMode' is true.
            @see ImageProcessor#getPixelsCopy
            @see ImageProcessor#snapshot
        */
        public void setSnapshotCopyMode(bool b)
        {
            snapshotCopyMode = b;
        }

        /** Returns the number of color channels in the image. The color channels can be
        *  accessed by toFloat(channelNumber, fp) and written by setPixels(channelNumber, fp).
        * @return 1 for grayscale images, 3 for RGB images
        */
        public virtual int getNChannels()
        {
            return 1;   /* superseded by ColorProcessor */
        }

        /** Returns a FloatProcessor with the image or one color channel thereof.
        *  The roi and mask are also set for the FloatProcessor.
        *  @param channelNumber   Determines the color channel, 0=red, 1=green, 2=blue. Ignored for
        *                         grayscale images.
        *  @param fp     Here a FloatProcessor can be supplied, or null. The FloatProcessor
        *                         is overwritten when converting data (re-using its pixels array
        *                         improves performance).
        *  @return A FloatProcessor with the converted image data of the color channel selected
        */
        public abstract FloatProcessor toFloat(int channelNumber, FloatProcessor fp);

        /** Sets the pixels (of one color channel for RGB images) from a FloatProcessor.
        *  @param channelNumber   Determines the color channel, 0=red, 1=green, 2=blue.Ignored for
        *                         grayscale images.
        *  @param fp              The FloatProcessor where the image data are read from.
        */
        public abstract void setPixels(int channelNumber, FloatProcessor fp);

        /** Returns a reference to this image's pixel array. The
        array type (byte[], short[], float[] or int[]) varies
        depending on the image type. */
        public abstract object getPixels();

        /** Returns a copy of the pixel data. Or returns a reference to the
            snapshot buffer if it is not null and 'snapshotCopyMode' is true.
            @see ImageProcessor#snapshot
            @see ImageProcessor#setSnapshotCopyMode
        */
        public abstract object getPixelsCopy();

        /** Returns the value of the pixel at (x,y). For RGB images, the
         * argb values are packed in an int. For float images, the
         * the value must be converted using Float.intBitsToFloat().
         * Returns zero if either the x or y coodinate is out of range.
         * Use <i>getValue(x,y)</i> to get calibrated values from
         * 8-bit and 16-bit images, to get intensity values from RGB
         * images and to get float values from 32-bit images.
         * @see ImageProcessor#getValue
        */
        public abstract int getPixel(int x, int y);

        /** This is a faster version of getPixel() that does not do bounds checking. */
        public abstract int get(int x, int y);

        public abstract int get(int index);

        /** This is a faster version of putPixel() that does not clip
            out of range values and does not do bounds checking. */
        public abstract void set(int x, int y, int value);

        public abstract void set(int index, int value);

        /** Returns the value of the pixel at (x,y) as a float. Faster
         * than getPixelValue() but does no bounds checking and
         * does not return calibrated values.
        */
        public abstract float getf(int x, int y);

        public abstract float getf(int index);

        /** Sets the value of the pixel at (x,y) to 'value'. Does no bounds
         * checking or clamping, making it faster than putPixel(). Due to the lack
         * of bounds checking, (x,y) coordinates outside the image may cause
         * an exception. Due to the lack of clamping, values outside the 0-255
         * range (for byte) or 0-65535 range (for short) are not handled correctly.
        */
        public abstract void setf(int x, int y, float value);

        public abstract void setf(int index, float value);

        public int getPixelCount()
        {
            return width*height;
        }

        /** Returns a copy of the pixel data as a 2D int array with
        dimensions [x=0..width-1][y=0..height-1]. With RGB
        images, the returned values are in packed ARGB format.
        With float images, the returned values must be converted
        to float using Float.intBitsToFloat(). */
        public int[,] getIntArray()
        {
            int[,]a = new int[width, height];
            for (int y = 0; y<height; y++)
            {
                for (int x = 0; x<width; x++)
                    a[x,y]=get(x, y);
            }
            return a;
        }

        /** Returns the samples for the pixel at (x,y) in an int array.
         * RGB pixels have three samples, all others have one.
         * Returns zeros if the the coordinates are not in bounds.
         * iArray is an optional preallocated array.
        */
        public virtual int[] getPixel(int x, int y, int[] iArray)
        {
            if (iArray == null) iArray = new int[1];
            iArray[0] = getPixel(x, y);
            return iArray;
        }

        /** Sets a pixel in the image using an int array of samples.
            RGB pixels have three samples, all others have one. */
        public virtual void putPixel(int x, int y, int[] iArray)
        {
            putPixel(x, y, iArray[0]);
        }

        /** Uses the current interpolation method (bilinear or bicubic)
        to find the pixel value at real coordinates (x,y). */
        public abstract double getInterpolatedPixel(double x, double y);

        /** Uses the current interpolation method to find the pixel value at real coordinates (x,y).
        For RGB images, the argb values are packed in an int. For float images,
        the value must be converted using Float.intBitsToFloat().  Returns zero
        if the (x, y) is not inside the image. */
        public abstract int getPixelInterpolated(double x, double y);

        /** Uses bilinear interpolation to find the pixel value at real coordinates (x,y).
            Returns zero if the (x, y) is not inside the image. */
        public double getInterpolatedValue(double x, double y)
        {
            if (useBicubic)
                return getBicubicInterpolatedPixel(x, y, this);
            if (x<0.0 || x>=width-1.0 || y<0.0 || y>=height-1.0)
            {
                if (x<-1.0 || x>=width || y<-1.0 || y>=height)
                    return 0.0;
                else
                    return getInterpolatedEdgeValue(x, y);
            }
            int xbase = (int)x;
            int ybase = (int)y;
            double xFraction = x - xbase;
            double yFraction = y - ybase;
            if (xFraction<0.0) xFraction = 0.0;
            if (yFraction<0.0) yFraction = 0.0;
            double lowerLeft = getPixelValue(xbase, ybase);
            double lowerRight = getPixelValue(xbase+1, ybase);
            double upperRight = getPixelValue(xbase+1, ybase+1);
            double upperLeft = getPixelValue(xbase, ybase+1);
            double upperAverage = upperLeft + xFraction * (upperRight - upperLeft);
            double lowerAverage = lowerLeft + xFraction * (lowerRight - lowerLeft);
            return lowerAverage + yFraction * (upperAverage - lowerAverage);
        }

        /** This method is from Chapter 16 of "Digital Image Processing:
        An Algorithmic Introduction Using Java" by Burger and Burge
        (http://www.imagingbook.com/). */
        public virtual double getBicubicInterpolatedPixel(double x0, double y0, ImageProcessor ip2)
        {
            int u0 = (int)Math.Floor(x0);   //use floor to handle negative coordinates too
            int v0 = (int)Math.Floor(y0);
            if (u0 <= 0 || u0 >= width - 2 || v0 <= 0 || v0 >= height - 2)
                return ip2.getBilinearInterpolatedPixel(x0, y0);
            double q = 0;
            for (int j = 0; j <= 3; j++)
            {
                int v = v0 - 1 + j;
                double p = 0;
                for (int i = 0; i <= 3; i++)
                {
                    int u = u0 - 1 + i;
                    p = p + ip2.get(u, v) * cubic(x0 - u);
                }
                q = q + p * cubic(y0 - v);
            }
            return q;
        }

        public double getBilinearInterpolatedPixel(double x, double y)
        {
            if (x >= -1 && x < width && y >= -1 && y < height)
            {
                int method = interpolationMethod;
                interpolationMethod = BILINEAR;
                double value = getInterpolatedPixel(x, y);
                interpolationMethod = method;
                return value;
            }
            else
                return getBackgroundValue();
        }

        const double a = 0.5; // Catmull-Rom interpolation
        public static double cubic(double x)
        {
            if (x < 0.0) x = -x;
            double z = 0.0;
            if (x < 1.0)
                z = x * x * (x * (-a + 2.0) + (a - 3.0)) + 1.0;
            else if (x < 2.0)
                z = -a * x * x * x + 5.0 * a * x * x - 8.0 * a * x + 4.0 * a;
            return z;
        }

        private double getInterpolatedEdgeValue(double x, double y)
        {
            int xbase = (int)x;
            int ybase = (int)y;
            double xFraction = x - xbase;
            double yFraction = y - ybase;
            if (xFraction<0.0) xFraction = 0.0;
            if (yFraction<0.0) yFraction = 0.0;
            double lowerLeft = getEdgeValue(xbase, ybase);
            double lowerRight = getEdgeValue(xbase+1, ybase);
            double upperRight = getEdgeValue(xbase+1, ybase+1);
            double upperLeft = getEdgeValue(xbase, ybase+1);
            double upperAverage = upperLeft + xFraction * (upperRight - upperLeft);
            double lowerAverage = lowerLeft + xFraction * (lowerRight - lowerLeft);
            return lowerAverage + yFraction * (upperAverage - lowerAverage);
        }

        private float getEdgeValue(int x, int y)
        {
            if (x<=0) x = 0;
            if (x>=width) x = width-1;
            if (y<=0) y = 0;
            if (y>=height) y = height-1;
            return getPixelValue(x, y);
        }

        /** Stores the specified value at (x,y). Does
        nothing if (x,y) is outside the image boundary.
        For 8-bit and 16-bit images, out of range values
        are clamped. For RGB images, the
        argb values are packed in 'value'. For float images,
        'value' is expected to be a float converted to an int
        using Float.floatToIntBits(). */
        public abstract void putPixel(int x, int y, int value);

        /** Replaces each pixel with the 3x3 neighborhood mean. */
        public void smooth()
        {
            if (width > 1)
                filter(BLUR_MORE);
        }

        /** Sharpens the image or ROI using a 3x3 convolution kernel. */
        public void sharpen()
        {
            if (width > 1)
            {
                int[] kernel = {-1, -1, -1,
                    -1, 12, -1,
                    -1, -1, -1};
                convolve3x3(kernel);
            }
        }

        /** Finds edges in the image or ROI using a Sobel operator. */
        public virtual void findEdges()
        {
            if (width > 1)
                filter(FIND_EDGES);
        }

        /** Flips the image or ROI vertically. */
        public abstract void flipVertical();

        /** Flips the image or ROI horizontally. */
        public void flipHorizontal()
        {
            int[] col1 = new int[roiHeight];
            int[] col2 = new int[roiHeight];
            for (int x = 0; x < roiWidth / 2; x++)
            {
                getColumn(roiX + x, roiY, col1, roiHeight);
                getColumn(roiX + roiWidth - x - 1, roiY, col2, roiHeight);
                putColumn(roiX + x, roiY, col2, roiHeight);
                putColumn(roiX + roiWidth - x - 1, roiY, col1, roiHeight);
            }
        }

        /** Rotates the entire image 90 degrees clockwise. Returns
        a new ImageProcessor that represents the rotated image. */
        public ImageProcessor rotateRight()
        {
            int width2 = height;
            int height2 = width;
            ImageProcessor ip2 = createProcessor(width2, height2);
            int[] arow = new int[width];
            for (int row = 0; row < height; row++)
            {
                getRow(0, row, arow, width);
                ip2.putColumn(width2 - row - 1, 0, arow, height2);
            }
            return ip2;
        }

        /** Rotates the entire image 90 degrees counter-clockwise. Returns
            a new ImageProcessor that represents the rotated image. */
        public ImageProcessor rotateLeft()
        {
            int width2 = height;
            int height2 = width;
            ImageProcessor ip2 = createProcessor(width2, height2);
            int[] arow = new int[width];
            int[] arow2 = new int[width];
            for (int row = 0; row < height; row++)
            {
                getRow(0, row, arow, width);
                for (int i = 0; i < width; i++)
                {
                    arow2[i] = arow[width - i - 1];
                }
                ip2.putColumn(row, 0, arow2, height2);
            }
            return ip2;
        }

        /** Returns the histogram of the image or ROI. Returns
            a luminosity histogram for RGB images and null
            for float images.
            <p>
            For 8-bit and 16-bit images, returns an array with one entry for each possible
            value that a pixel can have, from 0 to 255 (8-bit image) or 0-65535 (16-bit image).
            Thus, the array size is 256 or 65536, and the bin width in uncalibrated units is 1.
            <p>
            For RGB images, the brightness is evaluated using the color weights (which would result in a
            float value) and rounded to an int. This gives 256 bins. FloatProcessor.getHistogram is not
            implemented (returns null).
        */
        public abstract int[] getHistogram();


        /** Set the number of bins to be used for histograms of float images. */
        public void setHistogramSize(int size)
        {
            histogramSize = size;
            if (histogramSize < 1) histogramSize = 1;
        }

        /** Returns the number of float image histogram bins. The bin
            count is fixed at 256 for the other three data types. */
        public int getHistogramSize()
        {
            return histogramSize;
        }

        /** Set the range used for histograms of float images. The image range is
        used if both <code>histMin</code> and <code>histMax</code> are zero. */
        public void setHistogramRange(double histMin, double histMax)
        {
            if (histMin > histMax)
            {
                histMin = 0.0;
                histMax = 0.0;
            }
            histogramMin = histMin;
            histogramMax = histMax;
        }

        /**	Returns the minimum histogram value used for histograms of float images. */
        public double getHistogramMin()
        {
            return histogramMin;
        }

        /**	Returns the maximum histogram value used for histograms of float images. */
        public double getHistogramMax()
        {
            return histogramMax;
        }

        /** Inserts the image contained in 'ip' at (xloc, yloc). */
        public void insert(ImageProcessor ip, int xloc, int yloc)
        {
            copyBits(ip, xloc, yloc, Blitter.COPY);
        }


        /** Fills the image or ROI bounding rectangle with the current fill/draw value. Use
        *	fill(Roi) or fill(ip.getMask()) to fill non-rectangular selections.
        *	@see #setColor(Color)
        *	@see #setValue(double)
        *	@see #fill(Roi)
        */
        public virtual void fill()
        {
            process(FILL, 0.0);
        }

        /** Fills pixels that are within the ROI bounding rectangle and part of
        *	the mask (i.e. pixels that have a value=BLACK in the mask array).
        *	Use ip.getMask() to acquire the mask.
        *	Throws and IllegalArgumentException if the mask is null or
        *	the size of the mask is not the same as the size of the ROI.
        *	@see #setColor(Color)
        *	@see #setValue(double)
        *	@see #getMask
        *	@see #fill(Roi)
        */
        public abstract void fill(ImageProcessor mask);

        /**	 Fills the ROI with the current fill/draw value.
        *	@see #setColor(Color)
        *	@see #setValue(double)
        *	@see #fill(Roi)
        */
        public void fill(Roi roi)
        {
            if (roi!=null && roi.isLine())
            {
                if ((roi is Line) && roi.getStrokeWidth()>1 && !(roi is Arrow))
                    fillPolygon(roi.getPolygon());
                else
                    roi.drawPixels(this);
                return;
            }
            ImageProcessor m = getMask();
            Rectangle r = getRoi();
            setRoi(roi);
            fill(getMask());
            setMask(m);
            setRoi(r);
        }

        protected virtual void process(int op, double value)
        {
            double SCALE = 255.0 / Math.Log(255.0);
            int v;

            int[] lut = new int[256];
            for (int i = 0; i < 256; i++)
            {
                switch (op)
                {
                    case INVERT:
                        v = 255 - i;
                        break;
                    case FILL:
                        v = fgColor;
                        break;
                    case SET:
                        v = (int)value;
                        break;
                    case ADD:
                        v = i + (int)value;
                        break;
                    case MULT:
                        v = (int)Math.Round(i * value);
                        break;
                    case AND:
                        v = i & (int)value;
                        break;
                    case OR:
                        v = i | (int)value;
                        break;
                    case XOR:
                        v = i ^ (int)value;
                        break;
                    case GAMMA:
                        v = (int)(Math.Exp(Math.Log(i / 255.0) * value) * 255.0);
                        break;
                    case LOG:
                        if (i == 0)
                            v = 0;
                        else
                            v = (int)(Math.Log(i) * SCALE);
                        break;
                    case EXP:
                        v = (int)(Math.Exp(i / SCALE));
                        break;
                    case SQR:
                        v = i * i;
                        break;
                    case SQRT:
                        v = (int)Math.Sqrt(i);
                        break;
                    case MINIMUM:
                        if (i < value)
                            v = (int)value;
                        else
                            v = i;
                        break;
                    case MAXIMUM:
                        if (i > value)
                            v = (int)value;
                        else
                            v = i;
                        break;
                    default:
                        v = i;
                        break;
                }
                if (v < 0)
                    v = 0;
                if (v > 255)
                    v = 255;
                lut[i] = v;
            }
            applyTable(lut);
        }

        /**
         * Returns an array containing the pixel values along the
         * line starting at (x1,y1) and ending at (x2,y2). Pixel
         * values are sampled using getInterpolatedValue(double,double)
         * if interpolation is enabled or getPixelValue(int,int) if it is not.
         * For byte and short images, returns calibrated values if a
         * calibration table has been set using setCalibrationTable().
         * The length of the returned array, minus one, is approximately
         * equal to the length of the line.
         * @see ImageProcessor#setInterpolate
         * @see ImageProcessor#getPixelValue
         * @see ImageProcessor#getInterpolatedValue
        */
        public double[] getLine(double x1, double y1, double x2, double y2)
        {
            double dx = x2-x1;
            double dy = y2-y1;
            int n = (int)Math.Round(Math.Sqrt(dx*dx + dy*dy));
            double xinc = n>0 ? dx/n : 0;
            double yinc = n>0 ? dy/n : 0;
            if (!((xinc==0&&n==height) || (yinc==0&&n==width)))
                n++;
            double[] data = new double[n];
            double rx = x1;
            double ry = y1;
            if (interpolate)
            {
                for (int i = 0; i<n; i++)
                {
                    data[i] = getInterpolatedValue(rx, ry);
                    rx += xinc;
                    ry += yinc;
                }
            }
            else
            {
                for (int i = 0; i<n; i++)
                {
                    data[i] = getPixelValue((int)Math.Round(rx), (int)Math.Round(ry));
                    rx += xinc;
                    ry += yinc;
                }
            }
            return data;
        }

        /** Returns the pixel values along the horizontal line starting at (x,y). */
        public void getRow(int x, int y, int[] data, int length)
        {
            for (int i = 0; i<length; i++)
                data[i] = getPixel(x++, y);
        }

        /** Returns the pixel values along the horizontal line starting at (x,y). */
        public float[] getRow(int x, int y, float[] data, int length)
        {
            if (data==null)
                data = new float[length];
            for (int i = 0; i<length; i++)
                data[i] = getf(x++, y);
            return data;
        }

        /** Returns the pixel values down the column starting at (x,y). */
        public void getColumn(int x, int y, int[] data, int length)
        {
            for (int i = 0; i<length; i++)
                data[i] = getPixel(x, y++);
        }

        /** Returns the pixel values down the column starting at (x,y). */
        public float[] getColumn(int x, int y, float[] data, int length)
        {
            if (data==null)
                data = new float[length];
            for (int i = 0; i<length; i++)
                data[i] = getf(x, y++);
            return data;
        }

        /** Inserts the pixels contained in 'data' into a
            horizontal line starting at (x,y). */
        public void putRow(int x, int y, int[] data, int length)
        {
            for (int i = 0; i<length; i++)
                putPixel(x++, y, data[i]);
        }

        /** Inserts the pixels contained in 'data' into a
            horizontal line starting at (x,y). */
        public void putRow(int x, int y, float[] data, int length)
        {
            for (int i = 0; i<length; i++)
                setf(x++, y, data[i]);
        }

        /** Inserts the pixels contained in 'data' into a
            column starting at (x,y). */
        public void putColumn(int x, int y, int[] data, int length)
        {
            for (int i = 0; i<length; i++)
                putPixel(x, y++, data[i]);
        }

        /** Inserts the pixels contained in 'data' into a
            column starting at (x,y). */
        public void putColumn(int x, int y, float[] data, int length)
        {
            for (int i = 0; i<length; i++)
                setf(x, y++, data[i]);
        }

        /**
        Sets the current drawing location.
        @see ImageProcessor#lineTo
        @see ImageProcessor#drawString
        */
        public void moveTo(int x, int y)
        {
            cx = x;
            cy = y;
        }

        /** Sets the line width used by lineTo() and drawDot(). */
        public void setLineWidth(int width)
        {
            lineWidth = width;
            if (lineWidth < 1) lineWidth = 1;
        }

        /** Returns the current line width. */
        public int getLineWidth()
        {
            return lineWidth;
        }

        /** Draws a line from the current drawing location to (x2,y2). */
        public void lineTo(int x2, int y2)
        {
            int xMin = clipXMin - lineWidth / 2 - 1;  //need not draw dots outside of this rect
            int xMax = clipXMax + lineWidth / 2 + 1;
            int yMin = clipYMin - lineWidth / 2 - 1;
            int yMax = clipYMax + lineWidth / 2 + 1;
            int dx = x2 - cx;
            int dy = y2 - cy;
            int absdx = dx >= 0 ? dx : -dx;
            int absdy = dy >= 0 ? dy : -dy;
            int n = absdy > absdx ? absdy : absdx;
            double xinc = dx != 0 ? (double)dx / n : 0; //single point (dx=dy=n=0): avoid division by zero
            double yinc = dy != 0 ? (double)dy / n : 0;
            double x = cx;
            double y = cy;
            cx = x2; cy = y2;       //keep end point as starting for the next lineTo
            int i1 = 0;
            if (dx > 0)
                i1 = Math.Max(i1, (int)((xMin - x) / xinc));
            else if (dx < 0)
                i1 = Math.Max(i1, (int)((xMax - x) / xinc));
            else if (x < xMin || x > xMax)
                return; // vertical line outside y range
            if (dy > 0)
                i1 = Math.Max(i1, (int)((yMin - y) / yinc));
            else if (dy < 0)
                i1 = Math.Max(i1, (int)((yMax - y) / yinc));
            else if (y < yMin || y > yMax)
                return; // horizontal line outside y range
            int i2 = n;
            if (dx > 0)
                i2 = Math.Min(i2, (int)((xMax - x) / xinc));
            else if (dx < 0)
                i2 = Math.Min(i2, (int)((xMin - x) / xinc));
            if (dy > 0)
                i2 = Math.Min(i2, (int)((yMax - y) / yinc));
            else if (dy < 0)
                i2 = Math.Min(i2, (int)((yMin - y) / yinc));
            x += i1 * xinc;
            y += i1 * yinc;
            for (int i = i1; i <= i2; i++)
            {
                if (lineWidth == 1)
                    drawPixel((int)Math.Round(x), (int)Math.Round(y));
                else if (lineWidth == 2)
                    drawDot2((int)Math.Round(x), (int)Math.Round(y));
                else
                    drawDot((int)Math.Round(x), (int)Math.Round(y));
                x += xinc;
                y += yinc;
            }
        }

        /** Draws a line from (x1,y1) to (x2,y2). */
        public void drawLine(int x1, int y1, int x2, int y2)
        {
            moveTo(x1, y1);
            lineTo(x2, y2);
        }

        /** Draws a rectangle. */
        public void drawRect(int x, int y, int width, int height)
        {
            if (width<1 || height<1)
                return;
            if (lineWidth==1)
            {
                moveTo(x, y);
                lineTo(x+width-1, y);
                lineTo(x+width-1, y+height-1);
                lineTo(x, y+height-1);
                lineTo(x, y);
            }
            else
            {
                moveTo(x, y);
                lineTo(x+width, y);
                lineTo(x+width, y+height);
                lineTo(x, y+height);
                lineTo(x, y);
            }
        }

        /** Fills a rectangle. */
        public void fillRect(int x, int y, int width, int height)
        {
            setRoi(x, y, width, height);
            fill();
            resetRoi();
        }

        /** Draws a polygon. */
        public void drawPolygon(Polygon p)
        {
            moveTo(p.xpoints[0], p.ypoints[0]);
            for (int i = 0; i<p.npoints; i++)
                lineTo(p.xpoints[i], p.ypoints[i]);
            lineTo(p.xpoints[0], p.ypoints[0]);
        }

        /** Fills a polygon. */
        public void fillPolygon(Polygon p)
        {
            setRoi(p);
            fill(getMask());
            resetRoi();
        }

        /** @deprecated */
        public void drawDot2(int x, int y)
        {
            drawPixel(x, y);
            drawPixel(x - 1, y);
            drawPixel(x, y - 1);
            drawPixel(x - 1, y - 1);
        }

        /** Draws a dot using the current line width and fill/draw value. */
        public void drawDot(int xcenter, int ycenter)
        {
            double r = lineWidth / 2.0;
            int xmin = (int)(xcenter - r + 0.5), ymin = (int)(ycenter - r + 0.5);
            int xmax = xmin + lineWidth, ymax = ymin + lineWidth;
            //if (xcenter<clipXMin || ycenter<clipYMin || xcenter>clipXMax || ycenter>clipYMax ) {
            if (xmin < clipXMin || ymin < clipYMin || xmax > clipXMax || ymax > clipYMax)
            {
                // draw edge dot
                double r2 = r * r;
                r -= 0.5;
                double xoffset = xmin + r, yoffset = ymin + r;
                double xx, yy;
                for (int y = ymin; y < ymax; y++)
                {
                    for (int x = xmin; x < xmax; x++)
                    {
                        xx = x - xoffset; yy = y - yoffset;
                        if (xx * xx + yy * yy <= r2)
                            drawPixel(x, y);
                    }
                }
            }
            else
            {
                //todo:
                // if (dotMask == null || lineWidth != dotMask.getWidth())
                // {
                //     OvalRoi oval = new OvalRoi(0, 0, lineWidth, lineWidth);
                //     dotMask = oval.getMask();
                // }
                // setRoi(xmin, ymin, lineWidth, lineWidth);
                // fill(dotMask);
                // roiX = 0; roiY = 0; roiWidth = width; roiHeight = height;
                // xMin = 1; xMax = width - 2; yMin = 1; yMax = height - 2;
                // mask = null;
            }
        }

        private void setupFontMetrics()
        {
            if (fontMetrics == null)
            {
                fmImage = new Bitmap(1, 1);
                fmGraphics = Graphics.FromImage(fmImage);
                // fmImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
                // fmGraphics = (Graphics2D)fmImage.getGraphics();
                // fmGraphics.setFont(font);
                // Java2.setAntialiasedText(fmGraphics, antialiasedText);
                // fontMetrics = fmGraphics.getFontMetrics(font);
                fontMetrics = new FontMetrics(fmGraphics, font);
            }
        }

        /** Draws a string at the current drawing location using the current fill/draw value.
         *  Draws multiple lines if the string contains newline characters.
         *  The current x coordinate is the left/center/right end of the string for
         *  left/center/right justification.
         *  The current y coordinate determines the bottommost position of the string,
         *  including the descent of the font (i.e., characters reaching below the baseline)
         *  For multi-line strings, the y coordinate applies to the first line.
         *  The y of the drawing position is incremented by the height of one text line,
         *  i.e. points at the drawing position for the next text line */
        public void drawString(string s)
        {
            if (s == null || s.Equals("")) return;
            setupFontMetrics();
            if (s.IndexOf("\n") == -1)
                drawString2(s);
            else
            {
                string[] s2 = s.Split("\n"); //Tools.split(s, "\n");
                for (int i = 0; i < s2.Length; i++)
                    drawString2(s2[i]);
            }
        }

        /** Draws a single-line string at the current drawing location cx, cy and
         *  adds the line height (FontMetrics.getHeight) to the current y coordinate 'cy' */
        private void drawString2(string s)
        {
            int w = getStringWidth(s);
            int cxx = cx;
            if (justification == CENTER_JUSTIFY)
                cxx -= w / 2;
            else if (justification == RIGHT_JUSTIFY)
                cxx -= w;
            int h = fontMetrics.getHeight();
            if (w <= 0 || h <= 0) return;
            Image bi = new Bitmap(w, h); // BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
            Graphics g = Graphics.FromImage(bi);  //bi.getGraphics();
            FontMetrics metrics = new FontMetrics(g, font); //g.getFontMetrics(font);
            int fontHeight = metrics.getHeight();
            int descent = metrics.getDescent();
            // g.setFont(font);

            if (antialiasedText && cxx >= 0 && cy - h >= 0)
            {
                // Java2.setAntialiasedText(g, true);
                setRoi(cxx, cy - h, w, h);
                ImageProcessor ip = crop();
                resetRoi();
                if (ip.getWidth() == 0 || ip.getHeight() == 0)
                    return;
                g.DrawImage(ip.createImage(), new PointF(0,0));
                //g.setColor(drawingColor);
                Brush brush = new SolidBrush(drawingColor);
                g.DrawString(s, font, brush, 0, h - descent);
                g.Dispose();
                ip = new ColorProcessor(bi);
                if (this is ByteProcessor) {
                    ip = ip.convertToByte(false);
                    if (isInvertedLut()) ip.invert();
                }
                //new ij.ImagePlus("ip",ip).show();
                insert(ip, cxx, cy - h);
                cy += h;
                return;
            }

            {
                // Java2.setAntialiasedText(g, false);
                Brush brush = new SolidBrush(Color.White);
                g.FillRectangle(brush, 0, 0, w, h);

                brush = new SolidBrush(Color.Black);
                //不加下面这句，绘制出来的字体，粗并且胡成一团
                g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
                g.DrawString(s,font, brush, 0,  descent);
                g.Dispose();
                ImageProcessor ip = new ColorProcessor(bi);
                ImageProcessor textMask = ip.convertToByte(false);
                byte[] mpixels = (byte[])textMask.getPixels();
                // new ImagePlus("textmask",textMask).Show();
                textMask.invert();
                if (cxx < width && cy - h < height)
                {
                    setMask(textMask);
                    setRoi(cxx, cy - h, w, h);
                    //todo:
                    fill(getMask());
                }

                resetRoi();
                cy += h;
            }
        }

        /** Draws a string at the specified location using the current fill/draw value.
         *  Draws multiple lines if the string contains newline characters.
         *  The x coordinate is the left/center/right end of the string for left/center/right
         *  justification.
         *  The y coordinate determines the bottommost position of the string,
         *  including the descent of the font (i.e., characters reaching below the baseline)
         *  For multi-line strings, the y coordinate applies to the first line. */
        public void drawString(string s, int x, int y)
        {
            moveTo(x, y);
            drawString(s);
        }

        /** Draws a string at the specified location with a filled background.
            A JavaScript example is available at
                http://imagej.nih.gov/ij/macros/js/DrawTextWithBackground.js
        */
        public void drawString(string s, int x, int y, Color background)
        {
            Color foreground = drawingColor;
            FontMetrics metrics = getFontMetrics();
            int w = 0;
            int h = metrics.getAscent() + metrics.getDescent();
            int y2 = y;
            if (s.IndexOf("\n") != -1)
            {
                string[] s2 = s.Split("\n"); //Tools.split(s, "\n");
                for (int i = 0; i < s2.Length; i++)
                {
                    int w2 = getStringWidth(s2[i]);
                    if (w2 > w) w = w2;
                }
                int h2 = metrics.getHeight();
                y2 += h2 * (s2.Length - 1);
                h += h2 * (s2.Length - 1);
            }
            else
                w = getStringWidth(s);
            int x2 = x;
            if (justification == CENTER_JUSTIFY)
                x2 -= w / 2;
            else if (justification == RIGHT_JUSTIFY)
                x2 -= w;
            setColor(background);
            setRoi(x2, y2 - h, w, h);
            fill();
            resetRoi();
            setColor(foreground);
            drawString(s, x, y);
        }

        /** Sets the justification used by drawString(), where <code>justification</code>
        is CENTER_JUSTIFY, RIGHT_JUSTIFY or LEFT_JUSTIFY. The default is LEFT_JUSTIFY. */
        public void setJustification(int justification)
        {
            if (justification<LEFT_JUSTIFY || justification>RIGHT_JUSTIFY)
                justification = LEFT_JUSTIFY;
            this.justification = justification;
        }

        private ImageProcessor dotMask;

        /** Transforms the image or ROI using a lookup table. The
        length of the table must be 256 for byte images and
        65536 for short images. RGB and float images are not
        supported. */
        public abstract void applyTable(int[] lut);


        #region Operation
        /** Inverts the image or ROI. */
        public virtual void invert() { process(INVERT, 0.0); }

        /** Adds 'value' to each pixel in the image or ROI. */
        public virtual void add(int value) { process(ADD, value); }

        /** Adds 'value' to each pixel in the image or ROI. */
        public virtual void add(double value) { process(ADD, value); }

        /** Subtracts 'value' from each pixel in the image or ROI. */
        public void subtract(double value)
        {
            add(-value);
        }
        #endregion

        /** Returns an 8-bit version of this image as a ByteProcessor. */
        public ImageProcessor convertToByte(bool doScaling)
        {
            TypeConverter tc = new TypeConverter(this, doScaling);
            return tc.convertToByte();
        }

        /** Returns a 16-bit version of this image as a ShortProcessor. */
        public ImageProcessor convertToShort(bool doScaling)
        {
            TypeConverter tc = new TypeConverter(this, doScaling);
            return tc.convertToShort();
        }

        /** Returns a 32-bit float version of this image as a FloatProcessor.
            For byte and short images, converts using a calibration function
            if a calibration table has been set using setCalibrationTable(). */
        public ImageProcessor convertToFloat()
        {
            TypeConverter tc = new TypeConverter(this, false);
            return tc.convertToFloat(cTable);
        }

        /** Returns an RGB version of this image as a ColorProcessor. */
        public ImageProcessor convertToRGB()
        {
            TypeConverter tc = new TypeConverter(this, true);
            return tc.convertToRGB();
        }

        /** Returns an 8-bit version of this image as a ByteProcessor. 16-bit and 32-bit
         * pixel data are scaled from min-max to 0-255.
        */
        public ByteProcessor convertToByteProcessor()
        {
            return convertToByteProcessor(true);
        }

        /** Returns an 8-bit version of this image as a ByteProcessor. 16-bit and 32-bit
         * pixel data are scaled from min-max to 0-255 if 'scale' is true.
        */
        public ByteProcessor convertToByteProcessor(bool scale)
        {
            ByteProcessor bp;
            if (this is ByteProcessor)
                bp = (ByteProcessor)this.duplicate();
            else
                bp = (ByteProcessor)this.convertToByte(scale);
            return bp;
        }

        /** Returns a 16-bit version of this image as a ShortProcessor. 32-bit
         * pixel data are scaled from min-max to 0-255.
        */
        public ShortProcessor convertToShortProcessor()
        {
            return convertToShortProcessor(true);
        }

        /** Returns a 16-bit version of this image as a ShortProcessor. 32-bit
         * pixel data are scaled from min-max to 0-255 if 'scale' is true.
        */
        public ShortProcessor convertToShortProcessor(bool scale)
        {
            ShortProcessor sp;
            if (this is ShortProcessor)
                sp = (ShortProcessor)this.duplicate();
            else
                sp = (ShortProcessor)this.convertToShort(scale);
            return sp;
        }

        /** Returns a 32-bit float version of this image as a FloatProcessor.
        For byte and short images, converts using a calibration function
        if a calibration table has been set using setCalibrationTable(). */
        public FloatProcessor convertToFloatProcessor()
        {
            FloatProcessor fp;
            if (this is FloatProcessor)
                fp = (FloatProcessor)this.duplicate();
            else
                fp = (FloatProcessor)this.convertToFloat();
            return fp;
        }

        /** Returns an RGB version of this image as a ColorProcessor. */
        public ColorProcessor convertToColorProcessor()
        {
            ColorProcessor cp;
            if (this is ColorProcessor)
                cp = (ColorProcessor)this.duplicate();
            else
                cp = (ColorProcessor)this.convertToRGB();
            return cp;
        }

        /** Performs a convolution operation using the specified kernel.
        KernelWidth and kernelHeight must be odd. */
        public abstract void convolve(float[] kernel, int kernelWidth, int kernelHeight);

        /** Converts the image to binary using an automatically determined threshold.
            For byte and short images, converts to binary using an automatically determined
            threshold. For RGB images, converts each channel to binary. For
            float images, does nothing.
        */
        public virtual void autoThreshold()
        {
            threshold(getAutoThreshold());
        }

        /** Returns a pixel value (threshold) that can be used to divide the image into objects
        and background. It does this by taking a test threshold and computing the average
        of the pixels at or below the threshold and pixels above. It then computes the average
        of those two, increments the threshold, and repeats the process. Incrementing stops
        when the threshold is larger than the composite average. That is, threshold = (average
        background + average objects)/2. This description was posted to the ImageJ mailing
        list by Jordan Bevic. */
        public int getAutoThreshold()
        {
            return getAutoThreshold(getHistogram());
        }

        /**	This is a version of getAutoThreshold() that uses a histogram passed as an argument. */
        public int getAutoThreshold(int[] histogram)
        {
            int level;
            int maxValue = histogram.Length - 1;
            double result, sum1, sum2, sum3, sum4;

            int count0 = histogram[0];
            histogram[0] = 0; //set to zero so erased areas aren't included
            int countMax = histogram[maxValue];
            histogram[maxValue] = 0;
            int min = 0;
            while ((histogram[min] == 0) && (min < maxValue))
                min++;
            int max = maxValue;
            while ((histogram[max] == 0) && (max > 0))
                max--;
            if (min >= max)
            {
                histogram[0] = count0; histogram[maxValue] = countMax;
                level = histogram.Length / 2;
                return level;
            }

            int movingIndex = min;
            int inc = Math.Max(max / 40, 1);
            do
            {
                sum1 = sum2 = sum3 = sum4 = 0.0;
                for (int i = min; i <= movingIndex; i++)
                {
                    sum1 += (double)i * histogram[i];
                    sum2 += histogram[i];
                }
                for (int i = (movingIndex + 1); i <= max; i++)
                {
                    sum3 += (double)i * histogram[i];
                    sum4 += histogram[i];
                }
                result = (sum1 / sum2 + sum3 / sum4) / 2.0;
                movingIndex++;
            } while ((movingIndex + 1) <= result && movingIndex < max - 1);

            histogram[0] = count0; histogram[maxValue] = countMax;
            level = (int)Math.Round(result);
            return level;
        }

        /** Updates the clipping rectangle used by lineTo(), drawLine(), drawDot() and drawPixel().
        The clipping rectangle is reset by passing a null argument or by calling resetRoi(). */
        public void setClipRect(Rectangle clipRect)
        {
            if (clipRect == Rectangle.Empty)
            {
                clipXMin = 0;
                clipXMax = width - 1;
                clipYMin = 0;
                clipYMax = height - 1;
            }
            else
            {
                clipXMin = clipRect.X;
                clipXMax = clipRect.X + clipRect.Width - 1;
                clipYMin = clipRect.Y;
                clipYMax = clipRect.Y + clipRect.Height - 1;
                if (clipXMin < 0) clipXMin = 0;
                if (clipXMax >= width) clipXMax = width - 1;
                if (clipYMin < 0) clipYMin = 0;
                if (clipYMax >= height) clipYMax = height - 1;
            }
        }

        protected String maskSizeError(ImageProcessor mask)
        {
            return "Mask size (" + mask.getWidth() + "x" + mask.getHeight() + ") != ROI size (" +
                   roiWidth + "x" + roiHeight + ")";
        }


        // method and variables used by updateComposite()
        public virtual byte[] create8BitImage() { return null; }

        /** Convolves the image or ROI with the specified
        3x3 integer convolution kernel. */
        public abstract void convolve3x3(int[] kernel);

        /** A 3x3 filter operation, where the argument (BLUR_MORE,  FIND_EDGES,
             MEDIAN_FILTER, MIN or MAX) determines the filter type. */
        public abstract void filter(int type);

        /** A 3x3 median filter. Requires 8-bit or RGB image. */
        public abstract void medianFilter();

        /** Adds pseudorandom, Gaussian ("normally") distributed values, with
        mean 0.0 and the specified standard deviation, to this image or ROI. */
        public abstract void noise(double standardDeviation);

        /** Returns a new processor containing an image
        that corresponds to the current ROI. */
        public abstract ImageProcessor crop();

        /** Sets pixels less than or equal to level to 0 and all other
            pixels to 255. Only works with 8-bit and 16-bit images. */
        public abstract void threshold(int level);

        /** Returns a duplicate of this image. */
        public abstract ImageProcessor duplicate();

        /** Scales the image by the specified factors. Does not
            change the image size.
            @see ImageProcessor#setInterpolate
            @see ImageProcessor#resize
        */
        public abstract void scale(double xScale, double yScale);

        /** Returns a new ImageProcessor containing a scaled copy of this image or ROI.
            @see ij.process.ImageProcessor#setInterpolate
        */
        public abstract ImageProcessor resize(int dstWidth, int dstHeight);

        /** Returns a new ImageProcessor containing a scaled copy
            of this image or ROI, with the aspect ratio maintained. */
        public ImageProcessor resize(int dstWidth)
        {
            return resize(dstWidth, (int)(dstWidth * ((double)roiHeight / roiWidth)));
        }

        /** Returns a new ImageProcessor containing a scaled copy of this image or ROI.
            @param dstWidth   Image width of the resulting ImageProcessor
            @param dstHeight  Image height of the resulting ImageProcessor
            @param useAverging  True means that the averaging occurs to avoid
                aliasing artifacts; the kernel shape for averaging is determined by
                the interpolationMethod. False if subsampling without any averaging
                should be used on downsizing.  Has no effect on upsizing.
            @see ImageProcessor#setInterpolationMethod
            Author: Michael Schmid
        */
        public ImageProcessor resize(int dstWidth, int dstHeight, bool useAverging)
        {
            Rectangle r = getRoi();
            int rWidth = r.Width;
            int rHeight = r.Height;
            if ((dstWidth >= rWidth && dstHeight >= rHeight) || !useAverging)
                return resize(dstWidth, dstHeight);  //upsizing or downsizing without averaging
            else
            {  //downsizing with averaging in at least one direction: convert to float
                ImageProcessor ip2 = createProcessor(dstWidth, dstHeight);
                FloatProcessor fp = null;
                int channels = getNChannels();
                bool showStatus = getProgressIncrement(width, height) > 0;
                bool showProgres = showStatus && channels > 1;
                if (showProgres) showProgress(0.15);
                for (int channelNumber = 0; channelNumber < channels; channelNumber++)
                {
                    fp = toFloat(channelNumber, fp);
                    fp.setInterpolationMethod(interpolationMethod);
                    fp.setRoi(getRoi());
                    string msg = showStatus ? " (" + (channelNumber + 1) + "/" + channels + ")" : null;
                    FloatProcessor fp2 = fp.downsize(dstWidth, dstHeight, msg);
                    ip2.setPixels(channelNumber, fp2);
                    if (showProgres) showProgress(0.40 + 0.25 * channelNumber);
                }
                if (showProgres) showProgress(1.0);
                return ip2;
            }
        }

        /** Use linear interpolation to resize images that have a width or height of one. */
        public ImageProcessor resizeLinearly(int width2, int height2)
        {
            int bitDepth = getBitDepth();
            ImageProcessor ip1 = this;
            bool rotate = width == 1;
            if (rotate)
            {
                ip1 = ip1.rotateLeft();
                int w2 = width2;
                width2 = height2;
                height2 = w2;
            }
            ip1 = ip1.convertToFloat();
            int width1 = ip1.getWidth();
            int height1 = ip1.getHeight();
            ImageProcessor ip2 = ip1.createProcessor(width2, height2);
            double scale = (double)(width1 - 1) / (width2 - 1);
            float[] data1 = new float[width1];
            float[] data2 = new float[width2];
            ip1.getRow(0, 0, data1, width1);
            double fraction;
            for (int x = 0; x < width2; x++)
            {
                int x1 = (int)(x * scale);
                int x2 = x1 + 1;
                if (x2 == width1) x2 = width1 - 1;
                fraction = x * scale - x1;
                //ij.IJ.log(x+" "+x1+" "+x2+" "+fraction+" "+width1+" "+width2);
                data2[x] = (float)((1.0 - fraction) * data1[x1] + fraction * data1[x2]);
            }
            for (int y = 0; y < height2; y++)
                ip2.putRow(0, y, data2, width2);
            if (bitDepth == 8)
                ip2 = ip2.convertToByte(false);
            else if (bitDepth == 16)
                ip2 = ip2.convertToShort(false);
            if (rotate)
                ip2 = ip2.rotateRight();
            return ip2;
        }

        protected int getProgressIncrement(int w, int h)
        {
            if (progressBar == null)
                return 0;
            int inc = 0;
            int threshold = 15000000;
            if (interpolationMethod == BICUBIC)
                threshold = 5000000;
            bool isBig = w * h > threshold;
            if (isBig)
            {
                inc = h / 30;
                if (inc < 1) inc = 1;
            }
            return inc;
        }

        /** Returns 'true' if this is a binary image (8-bit-image with only 0 and 255). */
        public virtual bool isBinary()
        {
            return false;
        }

        /** Returns 'true' if this is a signed 16-bit image. */
        public bool isSigned16Bit()
        {
            return false;
        }

        /* This method is experimental and may be removed. */
        public static void setUseBicubic(bool b)
        {
            useBicubic = b;
        }

        /** Calculates and returns uncalibrated statistics for this image or ROI,
         * including histogram, area, mean, min and max, standard deviation,
         * and mode. Use the setRoi(Roi) method to limit statistics to
         * a non-rectangular area.
         * @return an {@link ij.process.ImageStatistics} object
         * @see #setRoi(Roi)
         * @see #getStatistics
         * @see ij.ImagePlus#getStatistics
        */
        public ImageStatistics getStats()
        {
            return ImageStatistics.getStatistics(this);
        }

        /** Calculates and returns complete uncalibrated (raw)
         * statistics for this image or ROI but it is up to 70 times
         * slower than getStats(). Use the setRoi(Roi) method to
         * limit statistics to a non-rectangular area.
         * @return an {@link ij.process.ImageStatistics} object
         * @see #setRoi(Roi)
         * @see #getStats
         * @see ij.ImagePlus#getAllStatistics
        */
        public ImageStatistics getStatistics()
        {
            return ImageStatistics.getStatistics(this, Measurements.ALL_STATS, null);
        }

        /** Blurs the image by convolving with a Gaussian function. */
        public void blurGaussian(double sigma)
        {
            resetRoi();
            GaussianBlur gb = new GaussianBlur();
            gb.showProgress(false);
            gb.blurGaussian(this, sigma);
        }

        /* Returns the PlugInFilter slice number. */
        public int getSliceNumber()
        {
            if (sliceNumber < 1)
                return 1;
            else
                return sliceNumber;
        }

        /** PlugInFilterRunner uses this method to set the slice number. */
        public void setSliceNumber(int slice)
        {
            sliceNumber = slice;
        }

        /** Returns a binary mask, or null if a threshold is not set or this is an RGB image.
         * @see ij.ImagePlus#createThresholdMask
         * @see ij.ImagePlus#createRoiMask
        */
        public virtual ByteProcessor createMask()
        {
            return null;
        }

        protected IndexColorModel getThresholdColorModel()
        {
            byte[] r = new byte[256];
            byte[] g = new byte[256];
            byte[] b = new byte[256];
            for (int i = 0; i < 255; i++)
            {
                r[i] = (byte)i;
                g[i] = (byte)i;
                b[i] = (byte)i;
            }
            r[255] = (byte)255;
            g[255] = (byte)0;
            b[255] = (byte)0;
            return new IndexColorModel(8, 256, r, g, b);
        }
    }
}
